import 'dart:async';
import 'dart:typed_data';

import 'package:bluetooth_ble/bluetooth_ble.dart';
import 'package:bluetooth_spp/bluetooth_spp.dart';
import 'package:sxw_bluetooth_provider/src/device/bluetooth_device.dart';
import 'package:sxw_bluetooth_provider/src/config/config.dart';
import 'package:sxw_bluetooth_provider/src/error/bt_error.dart';
import 'package:sxw_bluetooth_provider/src/utils/message_merger.dart';
import 'package:sxw_bluetooth_provider/src/utils/string_utils.dart';

import 'connection.dart';

class BleDelegate implements ConnectionHelper<SxwBleDevice, BleConfig> {
  final ConnectionDelegate delegate;

  BluetoothBle get ble => BluetoothBle();

  Spp get spp => Spp();

  BleDelegate(this.delegate);

  void connect(
      BleConfig config, Completer<SxwBluetoothDevice> completer) async {
    final device = ble.devices.firstWhere(
      (test) => StringUtils.equalsIgnoreCase(test.name, config.name),
      orElse: () => null,
    );

    if (device != null) {
      final connectionDelegate =
          _BleConnection(this, device, config, completer);
      connectionDelegate.connect();
      return;
    }

    final duration = config.scanTimeout;
    ble.scan(timeout: duration.inSeconds).then((data) {
      if (!data.any((test) => test.name == config.name)) {
        completer.completeError(BtErrors.deviceNotFound);
      }
    });

    StreamSubscription sub;

    sub = ble.deviceStream.listen((device) {
      if (StringUtils.equalsIgnoreCase(device?.name, config?.name)) {
        sub?.cancel();
        sub = null;
        final connectionDelegate =
            _BleConnection(this, device, config, completer);
        connectionDelegate.connect();
      }
    });
  }

  @override
  List<SxwBleDevice> get connectedDeviceList => _devices.toList();

  List<SxwBleDevice> _devices = [];

  bool onConnect(SxwBleDevice device) {
    if (_devices.any((test) => test.id == device.id)) {
      return false;
    }
    _devices.add(device);
    delegate.onConnectedDeviceChange();
    return true;
  }

  void onDisconnect(SxwBleDevice device) {
    if (device != null) {
      device.dispose();
      _devices.removeWhere((item) {
        return device.id == item.id;
      });
    }
    delegate.onConnectedDeviceChange();
  }

  void onConnectStateChanged(SxwBleDevice sxwBleDevice, bool isConnect) {
    if (isConnect) {
      onConnect(sxwBleDevice);
    } else {
      onDisconnect(sxwBleDevice);
    }
  }
}

class _BleConnection {
  final BleDevice device;
  final BleDelegate delegate;
  final BleConfig config;

  final Completer<SxwBluetoothDevice> completer;

  StreamSubscription _statusSub;
  StreamSubscription _dataSub;

  SxwBleDevice sxwBleDevice;

  bool _isConnect = false;

  bool get isConnect => _isConnect;

  set isConnect(bool isConnect) {
    if (isConnect != _isConnect) {
      onConnectStateChanged();
    }
    _isConnect = isConnect;
  }

  _BleConnection(
    this.delegate,
    this.device,
    this.config,
    this.completer,
  );

  void connect() {
    _statusSub = device.connectStateStream.listen((connected) {
      isConnect = connected;
    });
    device.connect();
  }

  void onConnectStateChanged() async {
    if (device.isConnect) {
      await device.requestMtu(512);
      sxwBleDevice = SxwBleDevice(
        bleDevice: device,
        config: config,
      );
      completer.complete(sxwBleDevice);
      onConnectDevice();
    } else {
      disableNotify();
    }
    delegate.onConnectStateChanged(sxwBleDevice, device.isConnect);
  }

  Future<void> onConnectDevice() async {
    final ch = await device.findCh(config.notifyServiceId, config.notifyChId);
    if (ch != null) {
      observeCh(ch);
    }
  }

  void observeCh(BleCh ch) {
    final merger = MessageMerger(notifier: (data) {
      sxwBleDevice?.onReceive(Uint8List.fromList(data));
    });

    ch.service.addListener(onChNotifyStateChange);

    device.changeNotify(ch, notify: true);

    _dataSub = device.notifyDataStream.listen((notifyData) {
      merger.addData(notifyData.data);
    });
  }

  Future<BleCh> notifyCh() {
    return device.findCh(config.notifyServiceId, config.notifyChId);
  }

  void onChNotifyStateChange() async {
    sxwBleDevice?.canUse = (await notifyCh()).notifying;
  }

  void disableNotify() {
    notifyCh().then((ch) {
      ch.service.removeListener(onChNotifyStateChange);
    });

    _dataSub?.cancel();
    _statusSub.cancel();
  }
}
